iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Software Development

文藝復興:這段程式碼,好像有點味道系列 第 27

Day 27|兩間畫室共用一把鑰匙:不適當的親密關係 (Inappropriate Intimacy)

  • 分享至 

  • xImage
  •  

依戀情結,是學徒天天跑去隔壁幫忙

不適當的親密關係,是更嚴重的下一步

兩間畫室,乾脆共用一把鑰匙,誰都能隨時進出對方的工作間
翻看對方還沒完成的草稿、隨手調整對方的顏料配方

表面上,兩間畫室還是各自獨立的招牌,實際上,早就分不清誰的作品是誰畫的
動一間畫室的規矩,另一間也會跟著亂

一個算購物車金額的方法,卻懂太多購物車的規則

購物車類別本身很單純:

public class Cart
{
    public int Qty { get; set; }
    public decimal UnitPrice { get; set; }
    public string DeliveryMethod { get; set; }
    public bool IsFragile { get; set; }
    public CustomerTier Tier { get; set; }
}

但實際算金額的邏輯,被寫在另一個類別裡:

public class CartPricingHelper
{
    public decimal CalculateSubtotal(Cart cart)
    {
        decimal subtotal = cart.Qty * cart.UnitPrice;

        // CartPricingHelper 知道「VIP 買滿 5 件打 9 折」這條 Cart 內部的定價規則
        if (cart.Tier == CustomerTier.Vip && cart.Qty >= 5)
        {
            subtotal *= 0.9m;
        }

        // CartPricingHelper 還知道「超商取貨的易碎品要加收處理費」這條規則
        if (cart.DeliveryMethod == "超商" && cart.IsFragile)
        {
            subtotal += 30;
        }

        return subtotal;
    }
}

兩個類別,共用了同一份業務規則

單看 CartPricingHelper,會發現它知道的事情,遠超過「算一個總金額」該知道的範圍

  • 它知道 VIP 的折扣門檻是「5 件」
  • 它知道折扣是「9 折」
  • 它知道超商取貨加易碎品,要加收「30 元」

這些數字跟規則,理論上該是 Cart 自己的秘密
CartPricingHelper 卻對它瞭若指掌,甚至比 Cart 自己還清楚

這帶來一個很實際的風險:Cart 的欄位或規則只要調整一點點,CartPricingHelper 就可能跟著壞掉,而且不會有任何警訊提醒你「這兩個類別,其實綁在一起」

這就是不適當的親密關係最危險的地方

兩個類別看起來各自獨立,實際上動一個,就會牽動另一個
而且這條牽連的線,沒有寫在任何介面上,只存在於某個工程師的腦子裡

把規則,收回真正的主人手上

解法同樣是搬移方法 (Move Method):把定價規則,搬回 Cart 自己的地盤

public class Cart
{
    public int Qty { get; set; }
    public decimal UnitPrice { get; set; }
    public string DeliveryMethod { get; set; }
    public bool IsFragile { get; set; }
    public CustomerTier Tier { get; set; }

    public decimal CalculateSubtotal()
    {
        decimal subtotal = Qty * UnitPrice;

        if (Tier == CustomerTier.Vip && Qty >= 5)
        {
            subtotal *= 0.9m;
        }

        if (DeliveryMethod == "超商" && IsFragile)
        {
            subtotal += 30;
        }

        return subtotal;
    }
}

CartPricingHelper 不再需要存在,呼叫端直接請 Cart 自己算:

var subtotal = cart.CalculateSubtotal();

現在只有 Cart 自己知道折扣門檻是多少、加收費用是多少
外部世界只需要知道「總金額算得出來」,不需要知道規則的每一個細節

比依戀情結更危險的情況:雙向的親密

依戀情結通常是單向的「一個方法,伸手向另一個類別要資料」

不適當的親密關係,經常是雙向的

A 存取 B 的內部細節,B 也反過來存取 A 的內部細節,兩者互相依賴對方的實作方式

這種雙向糾纏,是最危險的耦合形式
修改任何一邊,都可能牽動另一邊,卻沒有一個清楚的方向可以判斷「該從哪裡開始拆」

遇到這種情況,第一步通常不是急著搬移邏輯,而是先 「把雙向關聯改為單向」

決定哪一邊該是「知情的一方」,哪一邊該是「只透過公開介面詢問的一方」
先把關係理順,才有辦法談後續的重構

什麼時候,親密是被允許的

「子類別跟父類別之間的親密,是繼承關係本身就允許的」
子類別本來就該了解父類別暴露給它的保護成員

如果是靠委派 (Delegation) 硬是模擬出類似繼承的親密關係,那通常代表
直接改用繼承,會讓意圖更清楚

自我檢查清單

  1. 這個類別,是不是知道另一個類別內部的具體規則、門檻或數字?
  2. 如果 A 類別修改了自己的欄位,B 類別會不會跟著壞掉,卻沒有任何編譯期的警告?
  3. 這兩個類別之間的依賴,是單向的,還是雙向互相依賴?
  4. 要完整理解 A 類別的行為,是不是必須同時打開 B 類別的原始碼?
  5. 這份業務規則,如果只讓資料的擁有者自己知道,呼叫端還需要知道這麼多細節嗎?

明日預告

明天我們看一種更迂迴的耦合:一句簡單的請求,要接連問過好幾個物件,才能問到真正的答案

模組五第三站:訊息鏈(Message Chains)


上一篇
Day 26|學徒天天跑去隔壁畫室幫忙調色:依戀情結 (Feature Envy)
系列文
文藝復興:這段程式碼,好像有點味道27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言